-
Notifications
You must be signed in to change notification settings - Fork 82
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Retrieve bound images when staging new image #659
Conversation
lib/src/boundimage.rs
Outdated
use crate::task::Task; | ||
|
||
|
||
const BOOTC_SYSTEMD_DIR: &'static str = "/etc/systemd/system/bootc"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We chatted about this, I think it's OK but we could probably use an explicit tempdir::TempDir
too?
lib/src/boundimage.rs
Outdated
impl BoundImageManager { | ||
pub(crate) fn new(deployment: Deployment, sysroot: &SysrootLock) -> BoundImageManager { | ||
let deployment_dir = sysroot.deployment_dirpath(&deployment); | ||
let quadlet_unit_dir = "/".to_string() + deployment_dir.as_str() + BOOTC_QUADLET_DIR.to_string().as_str(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should work:
let quadlet_unit_dir = format!("/{deployment_dir}/{BOOTC_QUADLET_DIR}");
in crossplatform code:
Path::new("/").join(deployment_dir)
(but we don't care about crossplatform here)
lib/src/boundimage.rs
Outdated
Ok(_) => println!("Successfully synced images"), | ||
Err(e) => { | ||
self.clean_up()?; | ||
drop(e); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return Err(e)
instead is probably what we want here - this swallows
lib/src/boundimage.rs
Outdated
SYSTEMD_DIR, | ||
unit_name, | ||
); | ||
if !Path::new(systemd_dst.as_str()).exists() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's change the semantics here to:
- Move if it doens't exist
- If it does exist, only move if it changed
or so?
lib/src/boundimage.rs
Outdated
for bound_image in entries { | ||
let bound_image = bound_image?; | ||
let bound_image_path = bound_image.path(); | ||
let unit_name = bound_image_path.file_name().unwrap().to_str().unwrap(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something like:
let file_name = bound_image.file_name();
let Some(file_name) = file_name.to_str() else {
return Err(format!("Invalid filename: {file_name:?}"));
};
lib/src/boundimage.rs
Outdated
unit_name, | ||
); | ||
if !Path::new(systemd_dst.as_str()).exists() { | ||
fs::rename(bound_image_path.clone(), systemd_dst.clone())?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Borrow like &bound_image_path
lib/src/boundimage.rs
Outdated
match self.sync_images() { | ||
Ok(_) => println!("Successfully synced images"), | ||
Err(e) => { | ||
self.clean_up()?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be able to drop this when using the tempdir
- alternatively, we could use impl Drop
lib/src/task.rs
Outdated
@@ -76,6 +74,11 @@ impl Task { | |||
self | |||
} | |||
|
|||
pub(crate) fn env(mut self, k: &String, v: &String) -> Self { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should &str
and not &String
This comment was marked as off-topic.
This comment was marked as off-topic.
Sorry, something went wrong.
lib/src/boundimage.rs
Outdated
} | ||
|
||
impl BoundImageManager { | ||
pub(crate) fn new(deployment: Deployment, sysroot: &SysrootLock) -> BoundImageManager { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should likely be &Deployment
here (and borrow with &
at the call site)
2f5feaf
to
29d0b99
Compare
How do we deal with the following scenario? The new image ships with a new authfile and the |
To be clear this is a specific instance of the general question of what happens between overlap/conflict between options in the old and new .image file right? e.g. they could also conflict on Or actually the most obvious conflict: what happens when the One approach would be to change quadlet to support generating unique names for the units (a simple approach is to hash the input, i.e. we end up with But in general let's remember here - the goal is that the image is present. And it must already be present for the current root (barring some explicit admin action to prune)! Perhaps the simplest thing here is to add a prefix |
Ah, if I understand this right, there is a scenario where a new bootc image has a new bound .image file that references a AuthFile, e.g. So I think that leaves two options
Option 1 would ideally be implemented after #640 is in place. There is also a possibility of a conflict between podman and the storage format, e.g. podman 6 for some reason changes the storage format of Option 2 could still result in a failed pull if the "bootc-image" spec changes in the new image. I lean towards option 1. I'm not sure of all the implications of running in the new image's root though. |
Started playing around with trying to run systemd in a chroot or a container, I don't think that will be possible without a bunch of hacks (unless I'm missing some hidden systemd features). I'll start working on reading a separate spec file. |
I would lean towards erroring out if we detect any specifiers (scanning for any Taking I could imagine someone doing something with If we go down that path, in theory again we can parse the
That's actually the default when we just run a base bootc image today, right? It's what you get when you In theory we could switch to by default mounting each deployment in that way? Some cost/benefit there for sure. Today for example the previous deployment roots are just plain hardlinked dirtrees in the legacy ostree model. |
Although...it would be reasonable to want to add a new image and a pull secret for that image as a single step, so we likely should default to resolving any specified filesystem paths relative to the target root. This doesn't actually require us to run the target code in a container image though; we could resolve the paths and prepend the deployment root (or, change the logic so we open file descriptors for them, and pass them via the |
Ah, I was trying to use systemctl in a simple chroot but maybe running systemd directly in a container might work. Something like,
That seems reasonable to me. I'm not sure how common it is to use the specifiers and it should be a relatively easy thing for a user to replace in the image. |
Unless anyone objects, I'll continue with running in the existing root, restricting the use of specifiers, and expanding file paths to the new root. This should be the simplest/quickest thing to implement. If users complain about the lack of specifiers then we can add support for them by doing the container implementation. |
I do not want to derail the direction but I see a lot of corner cases being discussed which may cause pain in the future. I think that we won't run into those issues by introducing a new file I believe we need two fields:
I find this much simpler to document, implement and maintain. I am not married to the idea but want to motivate to reconsider. |
I'm fine with a new dedicated file too. Tangentially but important: any time we introduce config files we should apply the https://github.com/uapi-group/specifications/blob/main/specs/configuration_files_specification.md where it makes sense, and I think it does here. And it makes the most sense for these references to be in A new toml file with drop-ins in
seems fine to me too. |
@ckyrouac WDYT? |
I'll start working on using a separate image spec file. I'll try to make sure the spec is easy to adapt to new fields in the future. |
29d0b99
to
5cde25c
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! This is looking like good progress!
The other thing to figure out adding here is an integration test...let's chat about that.
} | ||
} | ||
|
||
fn validate_spec_value(value: &String) -> Result<()> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one seems very unit-testable
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should also use &str
here
lib/src/boundimage.rs
Outdated
} | ||
|
||
#[context("parse image file {file_name}")] | ||
fn parse_image_file(&self, file_name: &str, file_contents: &ini::Ini) -> Result<BoundImage> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could probably unit test this one too
lib/src/boundimage.rs
Outdated
|
||
#[context("pull bound images")] | ||
fn pull_images(&self, bound_images: Vec<BoundImage>) -> Result<()> { | ||
//TODO: do this in parallel |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we did do that it'd open up interesting questions around how we display progress bars. Are we doing that today via podman? I think the Task
bits may default stdin to /dev/null
? If we're not then we should probably explicitly provide e.g. -q
.
One thing I'm thinking here is perhaps we go ahead and generate a systemd unit for each container to pull (exactly like quadlet would do)?
But, clearly this is not a blocker for landing this PoC work.
5cde25c
to
6cd2b38
Compare
Thanks for the thorough review! It's very helpful to get to know rust style/idioms. I pushed all the suggested style related changes and will start on some unit tests now. For integration tests, I was planning to try to write some with the new tmt/nushell framework, if that makes sense? |
lib/src/boundimage.rs
Outdated
let mut file: File = rustix::fs::openat2( | ||
root_fd, | ||
file_path, | ||
OFlags::empty(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am the "missing O_CLOEXEC police" 😄
previously e.g.
I think so yep! Though so far one tangential thing is those tests tend not to pull much from the network, but I think we can just bite that bullet. |
6cd2b38
to
be03e70
Compare
be03e70
to
525e601
Compare
//TODO: do this in parallel | ||
for bound_image in bound_images { | ||
let mut task = Task::new("Pulling bound image", "/usr/bin/podman") | ||
.arg("pull") | ||
.arg(&bound_image.image); | ||
if let Some(auth_file) = &bound_image.auth_file { | ||
task = task.arg("--authfile").arg(format!("/{deployment_root}/{auth_file}")); | ||
task = task.arg("--authfile").arg(auth_file); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We had a live chat about this...dealing with the authfiles is tricky, because at this current point in the flow, we may not have done the "etc merge" and so the etc
that we see in the root will only have config from the image.
We may need to explicitly do a dance like "look for the auth file in the new root, fall back to current root"
Hmm cargo-deny is giving:
There's a few new dependency crates here. I think we can allowlist CC-1.0, but I need to check. Hmm wait..like why is |
Unfortunately...this one appears to be marginal. See https://docs.fedoraproject.org/en-US/legal/allowed-licenses/ Honestly, it's pretty annoying that someone hyper-optimized the ini parser to the point of having a specialized linked-list data structure that ends up transitively pulling in a custom implementation of Keccak (sha-3 family) for a use case we don't need. I can understand e.g. hyper-optimizing JSON parsing because yeah, sometimes you end up with gigabytes of JSON in the real world. But INI files? I don't think so. Looking around there's also tini - the code looks good to me. Spot checking the maintenance status too by looking at PRs I see e.g. this PR by a fellow RHT employee. Let's switch to this crate? |
bleh, thanks for digging into that. I'll switch to tini. |
9f8378a
to
8dc147f
Compare
alright - added more unit tests and more cleanup. I think we can get this in so people can start some initial testing with it. I'll continue working on the install path and integration tests separately. One significant change I made is to error out if an |
8dc147f
to
870b529
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some nits but I think we can get this in and gain experience and iterate from there!
Cargo.lock
Outdated
@@ -4,9 +4,9 @@ version = 3 | |||
|
|||
[[package]] | |||
name = "addr2line" | |||
version = "0.21.0" | |||
version = "0.22.0" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we have a full cargo update
here, I'll rebase this just on #669
let mut bar_file = td | ||
.create(format!("{CONTAINER_IMAGE_DIR}/bar.image")) | ||
.unwrap(); | ||
bar_file.write_all(b"[Image]\n").unwrap(); | ||
bar_file.write_all(b"Image=quay.io/bar/bar:latest").unwrap(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is totally fine as is, but would be shorter with e.g.:
td.write(format!("{CONTAINER_IMAGE_DIR}/bar.image"), "[Image]\nImage=quay.io/bar/bar:latest").unwrap();
} else if number_of_percents % 2 != 0 { | ||
anyhow::bail!("Systemd specifiers are not supported by bound bootc images: {value}"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, I don't think this is quite right, because if one has two specifiers then there'd still be an even number of %
. We want to check for %
not followed by another %
right?
I recently did some similar code over here: https://github.com/containers/composefs/blob/45e6179993aa8a057d27946e26f5f7306b6a1779/rust/composefs/src/dumpfile.rs#L116
Actually even more strongly than that, we do want to parse the string here and an %%
in the input should be evaluated to a single %
, so it should be much more like an "unescape" operation than a validation.
} | ||
} | ||
|
||
fn validate_spec_value(value: &String) -> Result<()> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should also use &str
here
|
||
//parse the file contents | ||
let path = Utf8Path::new(spec_dir).join(file_name); | ||
let mut file: File = rustix::fs::openat2( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Side note, I think we can use coreos/cap-std-ext#54 once that lands
lib/Cargo.toml
Outdated
@@ -30,7 +30,7 @@ liboverdrop = "0.1.0" | |||
libsystemd = "0.7" | |||
openssl = "^0.10.64" | |||
# TODO drop this in favor of rustix | |||
nix = { version = "0.29", features = ["ioctl", "sched"] } | |||
nix = { version = "0.29", features = ["ioctl", "sched", "fs"] } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this change may have only been needed in an earlier PR version? I dropped it...as the TODO says ideally we port to rustix.
This parses any file pointed to by a symlink with a .container or .image extension found in /usr/lib/bootc/bound-images.d. An error is thrown if a systemd specifier is found in the parsed fields. It currently only supports the Image and AuthFile fields. Some known shortcomings are that each image is pulled synchronously. It does not do any cleanup during a rollback or if the switch fails after pulling an image. The install path also needs to pull bound images. Co-authored-by: Colin Walters <[email protected]> Signed-off-by: Chris Kyrouac <[email protected]> Signed-off-by: Colin Walters <[email protected]>
870b529
to
71febde
Compare
Ah hmm a few currently gating clippy lints:
I pushed fixes for those. |
ci: fix failed log
Release 0.14.5
This is just a PoC using quadlet/systemd to pull "lifecycle" bound images.
I have a bootc image pushed to
quay.io/ckyrouac/bootc-lldb-bound:latest
for testing. Any custom bootc image with quadlet image files located in/etc/containers/systemd/bootc
should work.Steps for testing:
sudo bootc switch quay.io/ckyrouac/bootc-lldb-bound:latest
quay.io/ckyrouac/bootc-lldb
image was pulled viasudo podman images
sudo bootc status